package com.lzq.train.business.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.lzq.train.business.enums.RedisKeyPreEnum;
import com.lzq.train.business.mapper.cust.SkTokenMapperCust;
import com.lzq.train.common.resp.PageResp;
import com.lzq.train.common.util.SnowUtil;
import com.lzq.train.business.domain.SkToken;
import com.lzq.train.business.domain.SkTokenExample;
import com.lzq.train.business.mapper.SkTokenMapper;
import com.lzq.train.business.req.SkTokenQueryReq;
import com.lzq.train.business.req.SkTokenSaveReq;
import com.lzq.train.business.resp.SkTokenQueryResp;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class SkTokenService {

    private static final Logger LOG = LoggerFactory.getLogger(SkTokenService.class);

    @Resource
    private SkTokenMapper skTokenMapper;
    @Resource
    private SkTokenMapperCust skTokenMapperCust;
    @Resource
    private DailyTrainSeatService dailyTrainSeatService;
    @Resource
    private DailyTrainStationService dailyTrainStationService;
    @Resource
    private RedisTemplate redisTemplate;

    @Value("${spring.profiles.active}")
    private String env;

    public void save(SkTokenSaveReq req) {
        DateTime now = DateTime.now();
        SkToken skToken = BeanUtil.copyProperties(req, SkToken.class);
        if (ObjectUtil.isNull(skToken.getId())) {
            skToken.setId(SnowUtil.getSnowflakeNextId());
            skToken.setCreateTime(now);
            skToken.setUpdateTime(now);
            skTokenMapper.insert(skToken);
        } else {
            skToken.setUpdateTime(now);
            skTokenMapper.updateByPrimaryKey(skToken);
        }
    }

    public PageResp<SkTokenQueryResp> queryList(SkTokenQueryReq req) {
        SkTokenExample skTokenExample = new SkTokenExample();
        skTokenExample.setOrderByClause("id desc");
        SkTokenExample.Criteria criteria = skTokenExample.createCriteria();

        LOG.info("查询页码：{}", req.getPage());
        LOG.info("每页条数：{}", req.getSize());
        PageHelper.startPage(req.getPage(), req.getSize());
        List<SkToken> skTokenList = skTokenMapper.selectByExample(skTokenExample);

        PageInfo<SkToken> pageInfo = new PageInfo<>(skTokenList);
        LOG.info("总行数：{}", pageInfo.getTotal());
        LOG.info("总页数：{}", pageInfo.getPages());

        List<SkTokenQueryResp> list = BeanUtil.copyToList(skTokenList, SkTokenQueryResp.class);

        PageResp<SkTokenQueryResp> pageResp = new PageResp<>();
        pageResp.setTotal(pageInfo.getTotal());
        pageResp.setList(list);
        return pageResp;
    }

    public void delete(Long id) {
        skTokenMapper.deleteByPrimaryKey(id);
    }

    /**
     * 生成每日车次的令牌数量，也就是大概得车票数量
     * @param date
     * @param trainCode
     */
    public void genDaily(Date date, String trainCode) {
        // 首先删除当前令牌数据
        LOG.info("删除日期【{}】车次【{}】的令牌信息", DateUtil.formatDate(date), trainCode);
        SkTokenExample skTokenExample = new SkTokenExample();
        skTokenExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);

        // 获取车次的座位数 和 站数（座位数*站数=理想情况下的令牌数量，这里我们暂时考虑为理想的3/4）
        int seatStation = dailyTrainStationService.countStation(date, trainCode);
        LOG.info("车次【{}】的车站数：{}", trainCode, seatStation);

        int seatCount = dailyTrainSeatService.countSeat(date, trainCode, null);
        LOG.info("车次【{}】的座位数：{}", trainCode, seatCount);

        DateTime now = DateTime.now();
        SkToken skToken = new SkToken();
        skToken.setId(SnowUtil.getSnowflakeNextId());
        skToken.setDate(date);
        skToken.setTrainCode(trainCode);
        skToken.setCreateTime(now);
        skToken.setUpdateTime(now);

        // 令牌数量
        int tokenCount = (int)(seatCount * seatStation * 3 / 4);
        LOG.info("车次{}，令牌数量：{}", trainCode, tokenCount);
        skToken.setCount(tokenCount);

        skTokenMapper.insert(skToken);
    }

    /**
     * 抢票时：获取令牌
     * 问题：大量的请求进来都会操作数据库减少令牌，会造成数据库压力
     *      这里采用在内存中减少令牌数量，然后比如依次减少5个令牌才刷回数据库，减少数据库压力
     * @param date
     * @param trainCode
     * @return
     */
    public boolean validSkToken(Date date, String trainCode, long memberId) {
        LOG.info("会员【{}】获取日期【{}】开始校验令牌，车次：{}", memberId, DateUtil.formatDate(date), trainCode);

        if (!env.equals("dev")) {
            // 防止机器人刷票 使用令牌锁机制（注意这里完成后不能手动释放锁）
            String lockKey = RedisKeyPreEnum.SK_TOKEN + "-" + DateUtil.formatDate(date) + "-" + trainCode + "-" + memberId;
            Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 5, TimeUnit.SECONDS);
            if (Boolean.FALSE.equals(setIfAbsent)) {
                LOG.info("很遗憾没有抢到令牌锁：{}", lockKey);
                return false;
            } else {
                LOG.info("恭喜，抢到令牌锁：{}", lockKey);
            }
        }

        // 引入缓存 来实现扣减令牌 提高性能
        String skTokenKey = RedisKeyPreEnum.SK_TOKEN_COUNT + "-" + DateUtil.formatDate(date) + "-" + trainCode;
        Object skTokenCount = redisTemplate.opsForValue().get(skTokenKey);
        if (skTokenCount != null) {
            // 缓存中获取到了令牌
            LOG.info("缓存中有该车次令牌大闸的key：{}", skTokenKey);

            // 创建 序列化类 解决decrement方法运行错误
            GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
            // 序列化类，对象映射设置
            // 设置 value 的转化格式和 key 的转化格式
            redisTemplate.setValueSerializer(genericToStringSerializer);
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.afterPropertiesSet();

            Long count = redisTemplate.opsForValue().decrement(skTokenKey, 1);// 令牌数减去1
            if (count < 0L) {
                LOG.error("获取令牌失败：{}", skTokenKey);
                return false;
            } else {
                LOG.info("获取令牌后，令牌余数：{}", count);
                redisTemplate.expire(skTokenKey, 60, TimeUnit.SECONDS); // 刷新缓存过期时间
                // 每获取5个令牌更新一次数据库 减少数据库压力
                if (count % 5 == 0) {
                    // 这里设置缓存中令牌数减去5 后在 更新数据库
                    skTokenMapperCust.decrease(date, trainCode, 5);
                }
                return true;
            }
        } else {
            // 第一次缓存中没有当前车次的令牌 需要查询数据库
            LOG.info("缓存中没有该车次令牌大闸的key：{}", skTokenKey);
            SkTokenExample example = new SkTokenExample();
            example.createCriteria().andTrainCodeEqualTo(trainCode).andDateEqualTo(date);
            List<SkToken> trainSkToken = skTokenMapper.selectByExample(example);
            if (CollUtil.isEmpty(trainSkToken)) {
                LOG.info("找不到日期【{}】车次【{}】的令牌记录", DateUtil.formatDate(date), trainCode);
                return false;
            }
            SkToken skToken = trainSkToken.get(0);
            if (skToken.getCount() <= 0) {
                LOG.info("日期【{}】车次【{}】的令牌余量为0", DateUtil.formatDate(date), trainCode);
                return false;
            }
            // 有令牌 刷入缓存
            Integer count = skToken.getCount() - 1; // 令牌减一
            LOG.info("将该车次令牌大闸放入缓存中，key: {}， count: {}", skTokenKey, count);
            redisTemplate.opsForValue().set(skTokenKey, String.valueOf(count), 60, TimeUnit.SECONDS);

            return true;
        }

        // 以下代码为旧版本 数据库性能低
        // int updateCount = skTokenMapperCust.decrease(date, trainCode);
        // 如果更新记录数为1 则为获取令牌成功 否则令牌数量不够
        // return updateCount > 0;
    }
}
